﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

using System;
using Tesla.Bounding;
using Tesla.Content;
using Tesla.Math;

namespace Tesla.Graphics {
    /// <summary>
    /// A camera represents how we view the 3D scene. It handles setting/updating the 
    /// view and projection matrices and holds onto a viewport which the renderer uses to setup where and how the 3D scene
    /// is rendered to the screen. This allows for multiple cameras to exist, each containing all the details of how the scene
    /// is to be mapped to the 2D screen.
    /// </summary>
    public class Camera : ICamera {
        private String _name;
        private Matrix _view;
        private Matrix _proj;
        private Matrix _viewProj;
        private Viewport _viewport;
        private Vector3 _position;
        private Vector3 _direction;
        private Vector3 _up;
        private Vector3 _right;
        private ProjectionMode _projectionMode;
        private BoundingFrustum _frustum;

        /// <summary>
        /// Gets or sets the name of the camera.
        /// </summary>
        public String Name {
            get {
                return _name;
            }
            set {
                _name = value;
            }
        }

        /// <summary>
        /// Gets the view matrix of the camera.
        /// </summary>
        public Matrix ViewMatrix {
            get {
                return _view;
            }
        }

        /// <summary>
        /// Gets the projection matrix of the camera.
        /// </summary>
        public Matrix ProjectionMatrix {
            get {
                return _proj;
            }
        }

        /// <summary>
        /// Gets the View x Projection matrix.
        /// </summary>
        public Matrix ViewProjectionMatrix {
            get {
                return _viewProj;
            }
        }

        /// <summary>
        /// Gets or sets the viewport associated with the camera.
        /// </summary>
        public Viewport Viewport {
            get {
                return _viewport;
            }
            set {
                _viewport = value;
            }
        }

        /// <summary>
        /// Gets or sets the position of the camera in world space.
        /// </summary>
        public Vector3 Position {
            get {
                return _position;
            }
            set {
                _position = value;
            }
        }

        /// <summary>
        /// Gets or sets the direction the camera is facing in (forward vector of the camera's frame). This will be normalized automatically.
        /// </summary>
        public Vector3 Direction {
            get {
                return _direction;
            }
            set {
                _direction = value;
                _direction.Normalize();
            }
        }

        /// <summary>
        /// Gets or set the left vector of the camera's frame. This will be normalized automatically.
        /// </summary>
        public Vector3 Right {
            get {
                return _right;
            }
            set {
                _right = value;
                _right.Normalize();
            }
        }

        /// <summary>
        /// Gets or sets the up vector of the camera's frame. This will be normalized automatically.
        /// </summary>
        public Vector3 Up {
            get {
                return _up;
            }
            set {
                _up = value;
                _up.Normalize();
            }
        }

        /// <summary>
        /// Gets the projection mode of the camera. This can be set in the SetProjection methods.
        /// </summary>
        public ProjectionMode ProjectionMode {
            get {
                return _projectionMode;
            }
        }

        /// <summary>
        /// Instantiates a new camera.
        /// </summary>
        public Camera() {
            _name = "Camera";
            _viewport = new Viewport(0, 0, 1, 1);
            _position = new Vector3(0, 0, 0);
            _right = Vector3.Right;
            _up = Vector3.Up;
            _direction = Vector3.Forward;
            _projectionMode = ProjectionMode.Perspective;
            _frustum = new BoundingFrustum(Matrix.Identity);
        }

        /// <summary>
        /// Instantiates a new camera with the associated viewport.
        /// </summary>
        /// <param name="viewport">Viewport to bind to the camera</param>
        public Camera(Viewport viewport) {
            _name = "Camera";
            _viewport = viewport;
            _position = new Vector3(0, 0, 0);
            _right = Vector3.Right;
            _up = Vector3.Up;
            _direction = Vector3.Forward;
            _projectionMode = ProjectionMode.Perspective;
            _frustum = new BoundingFrustum(Matrix.Identity);
        }

        /// <summary>
        /// Instantiates a new camera with the specified name.
        /// </summary>
        /// <param name="name">Name of the camera</param>
        public Camera(String name) {
            _name = name;
            _viewport = new Viewport(0, 0, 1, 1);
            _position = new Vector3(0, 0, 0);
            _right = Vector3.Right;
            _up = Vector3.Up;
            _direction = Vector3.Forward;
            _projectionMode = ProjectionMode.Perspective;
            _frustum = new BoundingFrustum(Matrix.Identity);
        }

        /// <summary>
        /// Instantiates a new camera with the associated viewport and name.
        /// </summary>
        /// <param name="name">Name of the camera</param>
        /// <param name="viewport">Viewport to bind to the camera</param>
        public Camera(String name, Viewport viewport) {
            _name = name;
            _viewport = viewport;
            _position = new Vector3(0, 0, 0);
            _right = Vector3.Right;
            _up = Vector3.Up;
            _direction = Vector3.Forward;
            _projectionMode = ProjectionMode.Perspective;
            _frustum = new BoundingFrustum(Matrix.Identity);
        }

        /// <summary>
        /// Instantiates a new camera copied from the specified camera.
        /// </summary>
        /// <param name="camToClone">Camera to copy from</param>
        public Camera(ICamera camToClone) {
            _frustum = new BoundingFrustum(Matrix.Identity);
            Set(camToClone);
        }

        /// <summary>
        /// Sets this camera to match the values of the source camera.
        /// </summary>
        /// <param name="source">Source camera to copy from</param>
        public virtual void Set(ICamera source) {
            _name = source.Name;
            _viewport = source.Viewport;
            _position = source.Position;
            _right = source.Right;
            _up = source.Up;
            _direction = source.Direction;
            _projectionMode = source.ProjectionMode;
            _proj = source.ProjectionMatrix;
            _view = source.ViewMatrix;
            Update();
        }

        /// <summary>
        /// Set the camera's projection, either an orthographic camera or a perspective camera.
        /// </summary>
        /// <param name="mode">Projection mode, perspective or orthographic</param>
        /// <param name="width">Width of the view volume at the near view plane</param>
        /// <param name="height">Height of the view volume at the near view plane</param>
        /// <param name="near">Distance to the near view plane (or min-Z value if ortho)</param>
        /// <param name="far">Distance to the far view plane (or max-Z value if ortho)</param>
        public void SetProjection(ProjectionMode mode, float width, float height, float near, float far) {
            if(mode == ProjectionMode.Perspective) {
                _proj = Matrix.CreatePerspectiveMatrix(width, height, near, far);
            } else {
                _proj = Matrix.CreateOrthoMatrix(width, height, near, far);
            }
            _projectionMode = mode;
        }

        /// <summary>
        /// Sets the camera's projection. This will always create a perspective camera.
        /// </summary>
        /// <param name="fieldOfView">Field of view, in degrees</param>
        /// <param name="near">Distance to the near view plane</param>
        /// <param name="far">Distance to the far view plane</param>
        public void SetProjection(float fieldOfView, float near, float far) {
            _proj = Matrix.CreatePerspectiveFOVMatrix(MathHelper.ToRadians(fieldOfView), _viewport.AspectRatio, near, far);
            _projectionMode = ProjectionMode.Perspective;
        }

        /// <summary>
        /// Sets the camera's projection, either an orthographic camera or a perspective camera.
        /// </summary>
        /// <param name="mode">Projection mode, perspective or orthographic</param>
        /// <param name="left">Minimum x value of the view volume at the near view plane</param>
        /// <param name="right">Maximum x value of the view volume at the near view plane</param>
        /// <param name="bottom">Minimum y value of the view volume at the near view plane</param>
        /// <param name="top">Maximum y value of the view volume at the near view plane</param>
        /// <param name="near">Distance to the near view plane (or min-Z value if ortho)</param>
        /// <param name="far">Distance to the far view plane (or max-Z value if ortho)</param>
        public void SetProjection(ProjectionMode mode, float left, float right, float bottom, float top, float near, float far) {
            if(mode == ProjectionMode.Perspective) {
                _proj = Matrix.CreatePerspectiveMatrix(left, right, bottom, top, near, far);
            } else {
                _proj = Matrix.CreateOrthoMatrix(left, right, bottom, top, near, far);
            }
            _projectionMode = mode;
        }

        /// <summary>
        /// Sets the axes of the camera's frame. This will normalize the axes.
        /// </summary>
        /// <param name="right">Right vector</param>
        /// <param name="up">Up vector</param>
        /// <param name="direction">Forward vector</param>
        public void SetAxes(Vector3 right, Vector3 up, Vector3 direction) {
            _right = right;
            _right.Normalize();

            _up = up;
            _up.Normalize();

            _direction = direction;
            _direction.Normalize();
        }

        /// <summary>
        /// Sets the axes of the camera's frame from a quaternion rotation. This will normalize the axes.
        /// </summary>
        /// <param name="axes">Quaternion rotation</param>
        public void SetAxes(Quaternion axes) {
            Matrix temp;
            Matrix.FromQuaternion(ref axes, out temp);

            _right = temp.Right;
            _right.Normalize();

            _up = temp.Up;
            _up.Normalize();

            _direction = temp.Forward;
            _direction.Normalize();
        }

        /// <summary>
        /// Sets the axes of the camera's frame from a matrix rotation. This will normalize the axes.
        /// </summary>
        /// <param name="axes">Matrix rotation</param>
        public void SetAxes(Matrix axes) {
            _right = axes.Right;
            _right.Normalize();

            _up = axes.Up;
            _up.Normalize();

            _direction = axes.Forward;
            _direction.Normalize();
        }

        /// <summary>
        /// Set's the camera's frame. This will normalize the axes.
        /// </summary>
        /// <param name="position">Camera position</param>
        /// <param name="right">Right vector</param>
        /// <param name="up">Up vector</param>
        /// <param name="direction">Forward vector</param>
        public void SetFrame(Vector3 position, Vector3 right, Vector3 up, Vector3 direction) {
            _position = position;

            _right = right;
            _right.Normalize();

            _up = up;
            _up.Normalize();

            _direction = direction;
            _direction.Normalize();
        }

        /// <summary>
        /// Sets the camera's frame from the Quaternion rotation. This will normalize the axes.
        /// </summary>
        /// <param name="position">Camera position</param>
        /// <param name="axes">Quaterion rotation to get the axes from</param>
        public void SetFrame(Vector3 position, Quaternion axes) {
            _position = position;

            Matrix temp;
            Matrix.FromQuaternion(ref axes, out temp);

            _right = temp.Right;
            _right.Normalize();

            _up = temp.Up;
            _up.Normalize();

            _direction = temp.Forward;
            _direction.Normalize();
        }

        /// <summary>
        /// Set's the camera's frame from the Matrix rotation. This will normalize the axes.
        /// </summary>
        /// <param name="position">Camera position</param>
        /// <param name="axes">Matrix rotation to get the axes from</param>
        public void SetFrame(Vector3 position, Matrix axes) {
            _position = position;

            _right = axes.Right;
            _right.Normalize();

            _up = axes.Up;
            _up.Normalize();

            _direction = axes.Forward;
            _direction.Normalize();
        }

        /// <summary>
        /// Updates the camera's view matrix and bounding frustum.
        /// 
        /// This should be called whenever the projection matrix is changed or the camera's frame is changed.
        /// </summary>
        public virtual void Update() {
            //Temp _position + _direction
            Vector3 posd;
            Vector3.Add(ref _position, ref _direction, out posd);

            //Create the new view matrix
            Matrix.CreateViewMatrix(ref _position, ref posd, ref _up, out _view);

            //Create the View x Projection matrix
            Matrix.Multiply(ref _view, ref _proj, out _viewProj);

            //Update the frustum
            _frustum.SetMatrix(ref _viewProj);
        }

        /// <summary>
        /// Makes the camera look at the target.
        /// </summary>
        /// <param name="target">Target vector</param>
        /// <param name="worldUp">The up vector in the world</param>
        public void LookAt(Vector3 target, Vector3 worldUp) {
            //Ensure world up is normalized + valid
            worldUp.Normalize();
            if(worldUp.Equals(Vector3.Zero)) {
                worldUp = Vector3.Up;
            }

            //Find the direction
            Vector3 backward;
            Vector3.Subtract(ref _position, ref target, out backward);
            backward.Normalize();
            Vector3.Negate(ref backward, out _direction);

            //Find right axis
            Vector3.Cross(ref worldUp, ref backward, out _right);
            _right.Normalize();

            //Find local up axis
            Vector3.Cross(ref backward, ref _right, out _up);

            //Update the view matrix
            Update();
        }

        /// <summary>
        /// Fills the provided array with the 8 positions representing the corners of the camera's view frustum. The array
        /// must have the appropiate length (8 corners).
        /// </summary>
        /// <param name="array">Array to fill</param>
        /// <exception cref="System.ArgumentNullException">Thrown if the array is null.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the array is too small.</exception>
        public void GetFrustumCorners(Vector3[] array) {
            if(array == null) {
                throw new ArgumentNullException("array", "Array to save frustum corners to cannot be null.");
            }

            if(array.Length < 8) {
                throw new ArgumentOutOfRangeException("array", "Array to save frustum corners to must have a length of 8");
            }

            _frustum.GetCorners(array);
        }

        /// <summary>
        /// Returns an array of 8 positions, representing the corners of the camera's view frustum.
        /// </summary>
        /// <returns></returns>
        public Vector3[] GetFrustumCorners() {
            return _frustum.GetCorners();
        }

        /// <summary>
        /// Tests if a bounding volume is contained in the camera's frustum. Used to make
        /// cull checks. If the volume is null, it will always return ContainmentType.Inside
        /// </summary>
        /// <param name="volume">Volume to test</param>
        /// <returns>Result of the test</returns>
        public ContainmentType Contains(BoundingVolume volume) {
            //Assume we're inside to start with
            ContainmentType rval = ContainmentType.Inside;

            //If there is no volume, return
            if(volume == null) {
                return rval;
            }

            //Check each frustum plane
            for(int i = 0; i < 6; i++) {
                Plane plane;
                PlaneIntersectionType intersection;
                _frustum.GetPlane(ref i, out plane);
                volume.Intersects(ref plane, out intersection);
                switch(intersection) {
                    case PlaneIntersectionType.Back:
                        //It's outside one plane, therefore its entirely outside the frustum
                        return ContainmentType.Outside;
                    case PlaneIntersectionType.Front:
                        //Keep checking
                        break;
                    case PlaneIntersectionType.Intersects:
                        //If it intersects just one, set rval
                        rval = ContainmentType.Intersects;
                        break;
                }
            }

            return rval;
        }

        /// <summary>
        /// Gets the world position based on the x,y screen coordinates.
        /// </summary>
        /// <param name="screenPosition">Screen position (x,y) and z being the depth (0 - near, 1 - far planes)</param>
        /// <returns>Constructed world position</returns>
        public Vector3 GetWorldCoordinates(Vector3 screenPosition) {
            Matrix world = Matrix.Identity;

            Vector3 result;
            _viewport.UnProject(ref screenPosition, ref world, ref _view, ref _proj, out result);


            return result;
        }

        /// <summary>
        /// Gets the world position based on the x,y screen position.
        /// </summary>
        /// <param name="screenPosition">Screen position (x,y) and z being the depth (0 - near, 1 - far planes)</param>
        /// <param name="result">Result to store constructed world position</param>
        public void GetWorldCoordinates(ref Vector3 screenPosition, out Vector3 result) {
            Matrix world = Matrix.Identity;

            _viewport.UnProject(ref screenPosition, ref world, ref _view, ref _proj, out result);
        }

        /// <summary>
        /// Gets the screen position based on the world position.
        /// </summary>
        /// <param name="worldPosition">World position</param>
        /// <returns>Screen position</returns>
        public Vector2 GetScreenCoordinates(Vector3 worldPosition) {
            //Unproject it to get screen coordinates
            Matrix world = Matrix.Identity;

            //Unproject it to get screen coordinates
            Vector3 result;
            _viewport.Project(ref worldPosition, ref world, ref _view, ref _proj, out result);

            return new Vector2(result.X, result.Y);
        }

        /// <summary>
        /// Gets the screen position based on the world position.
        /// </summary>
        /// <param name="worldPosition">World position</param>
        /// <param name="result">Result to store screen position in</param>
        public void GetScreenCoordinates(ref Vector3 worldPosition, out Vector2 result) {
            //Treat the position as an absolute world position
            Matrix world = Matrix.Identity;

            //Unproject it to get screen coordinates
            Vector3 temp;
            _viewport.Project(ref worldPosition, ref world, ref _view, ref _proj, out temp);

            result.X = temp.X;
            result.Y = temp.Y;
        }

        /// <summary>
        /// Creates a ray to do picking tests, where the origin is on the near plane. This is the same as using
        /// GetWorldCoordinates for (x,y,0) and (x,y,1) and using the normalized difference as the ray's direction.
        /// </summary>
        /// <param name="screenPos">Screen position (x,y) and z being the depth (0 - near, 1 - far planes)</param>
        /// <returns>Resulting ray</returns>
        public Ray CreatePickRay(Vector2 screenPos) {
            Vector3 pos1 = new Vector3(screenPos.X, screenPos.Y, 0);
            Vector3 pos2 = new Vector3(screenPos.X, screenPos.Y, 1);

            Matrix world = Matrix.Identity;

            Vector3 posNear;
            _viewport.UnProject(ref pos1, ref world, ref _view, ref _proj, out posNear);

            Vector3 posFar;
            _viewport.UnProject(ref pos2, ref world, ref _view, ref _proj, out posFar);

            Vector3 dir;
            Vector3.Subtract(ref posFar, ref posNear, out dir);
            dir.Normalize();

            Ray ray;
            ray.Direction = dir;
            ray.Origin = posNear;

            return ray;
        }

        /// <summary>
        /// Creates a ray to do picking tests, where the origin is on the near plane. This is the same as using
        /// GetWorldCoordinates for (x,y,0) and (x,y,1) and using the normalized difference as the ray's direction.
        /// </summary>
        /// <param name="screenPos">Screen position (x,y) and z being the depth (0 - near, 1 - far planes)</param>
        /// <param name="result">Ray containing the result</param>
        public void CreatePickRay(ref Vector2 screenPos, out Ray result) {
            Vector3 pos1 = new Vector3(screenPos.X, screenPos.Y, 0);
            Vector3 pos2 = new Vector3(screenPos.X, screenPos.Y, 1);

            Matrix world = Matrix.Identity;

            Vector3 posNear;
            _viewport.UnProject(ref pos1, ref world, ref _view, ref _proj, out posNear);

            Vector3 posFar;
            _viewport.UnProject(ref pos2, ref world, ref _view, ref _proj, out posFar);

            Vector3 dir;
            Vector3.Subtract(ref posFar, ref posNear, out dir);
            dir.Normalize();

            result.Direction = dir;
            result.Origin = posNear;
        }

        /// <summary>
        /// Serializes the object and writes it to the output.
        /// </summary>
        /// <param name="output">Savable Output</param>
        public virtual void Write(ISavableWriter output) {
            output.Write("Name", _name);
            output.Write("Viewport", new Vector4(_viewport.X, _viewport.Y, _viewport.Width, _viewport.Height));
            output.Write("ViewportDepth", new Vector2(_viewport.MinDepth, _viewport.MaxDepth));
            output.Write("Position", _position);
            output.Write("Up", _up);
            output.Write("Direction", _direction);
            output.WriteEnum<ProjectionMode>("ProjectionMode", _projectionMode);
            output.Write("ProjectionMatrix", _proj);
        }

        /// <summary>
        /// Deserializes the object and populates it from the input.
        /// </summary>
        /// <param name="input">Savable input</param>
        public virtual void Read(ISavableReader input) {
            _name = input.ReadString();
            Vector4 vp = input.ReadVector4();
            Vector2 depth = input.ReadVector2();
            _viewport = new Viewport((int) vp.X, (int) vp.Y, (int) vp.Z, (int) vp.W);
            _viewport.MinDepth = depth.X;
            _viewport.MaxDepth = depth.Y;

            _position = input.ReadVector3();
            _up = input.ReadVector3();
            _direction = input.ReadVector3();
            _projectionMode = input.ReadEnum<ProjectionMode>();
            _proj = input.ReadMatrix();

            Update();
        }
    }
}
